<template>
  <div id="global-uploader" :class="{ 'global-uploader-single': !global }">
    <!-- 上传 -->
    <uploader
      ref="uploader"
      :options="initOptions"
      :fileStatusText="fileStatusText"
      :autoStart="false"
      @file-added="onFileAdded"
      @file-success="onFileSuccess"
      @file-progress="onFileProgress"
      @file-error="onFileError"
      class="uploader-app"
    >
      <uploader-unsupport></uploader-unsupport>

      <uploader-btn id="global-uploader-btn" ref="uploadBtn"
        >选择文件</uploader-btn
      >
      <!-- <uploader-btn id="global-uploader-btn" :directory="true" ref="uploadBtn"
        >选择文件夹</uploader-btn
      > -->

      <uploader-list v-show="panelShow">
        <div
          class="file-panel"
          slot-scope="props"
          :class="{ collapse: collapse }"
        >
          <div class="file-title">
            <div class="title">文件列表</div>
            <div class="operate">
              <el-button
                @click="collapse = !collapse"
                type="text"
                :title="collapse ? '展开' : '折叠'"
              >
                <i
                  class="iconfont"
                  :class="collapse ? 'el-icon-full-screen' : 'el-icon-minus'"
                ></i>
              </el-button>
              <el-button @click="close" type="text" title="关闭">
                <i class="el-icon-close"></i>
              </el-button>
            </div>
          </div>

          <ul class="file-list">
            <li
              class="file-item"
              :class="`file-${file.id}`"
              v-for="file in props.fileList"
              :key="file.id"
            >
              <uploader-file
                :class="'file_' + file.id"
                ref="files"
                :file="file"
                :list="true"
              ></uploader-file>
            </li>
            <div class="no-file" v-if="!props.fileList.length">
              <i class="iconfont icon-empty-file"></i> 暂无待上传文件
            </div>
          </ul>
        </div>
      </uploader-list>
    </uploader>
  </div>
</template>

<script>
/**
 *  全局上传插件，两种调用方式
 *   1. 作为全局页面的组件，使用event bus
 *   调用方法：Bus.$emit('openUploader', {params: {}, options: {}})
 *               params: 发送给服务器的额外参数；
 *               options：上传选项，目前支持 target、testChunks、mergeFn、accept
 *
 *   监听函数：Bus.$on('fileAdded', fn); 文件选择后的回调
 *           Bus.$on('fileSuccess', fn); 文件上传成功的回调，监听后记得释放
 *
 *   2. 作为普通组件在单个页面中调用，使用props
 */
import { ACCEPT_CONFIG } from "../assets/js/config.js";
import Bus from "../assets/js/bus.js";
import SparkMD5 from "spark-md5";
import { mergeSimpleUpload } from "../api/index.js";

export default {
  props: {
    global: {
      type: Boolean,
      default: true,
    },
    // 发送给服务器的额外参数
    params: {
      type: Object,
    },
    options: {
      type: Object,
    },
  },
  data() {
    return {
      initOptions: {
        target: "/dict/ClothingPices/AddFiles",
        chunkSize: "20480000", //分片上传后，每次分片大小
        fileParameterName: "file", //上传文件时文件的参数名
        maxChunkRetries: 3, //并发数
        testChunks: true, // 是否开启服务器分片校验
        // 服务器分片校验函数，秒传及断点续传基础
        checkChunkUploadedByResponse: function (chunk, message) {
          let skip = false;
          try {
            let objMessage = JSON.parse(message);
            if (objMessage.skipUpload) {
              skip = true;
            } else {
              skip = (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0;
            }
          } catch (e) {}
          return skip;
        },
        query: (file, chunk) => {
          return {
            ...file.params,
          };
        },
      },
      fileStatusText: {
        success: "上传成功",
        error: "上传失败",
        uploading: "上传中",
        paused: "已暂停",
        waiting: "等待上传",
      },
      panelShow: false, //选择文件后，展示上传panel
      collapse: false,
      customParams: {},
    };
  },
  watch: {
    params: {
      handler(data) {
        if (data) {
          this.customParams = data;
        }
      },
      immediate: true,
    },
    options: {
      handler(data) {
        if (data) {
          setTimeout(() => {
            this.customizeOptions(data);
          }, 0);
        }
      },
      immediate: true,
    },
  },
  mounted() {
    Bus.$on("openUploader", ({ params = {}, options = {} }) => {
      this.customParams = params;
      this.customizeOptions(options);
      if (this.$refs.uploadBtn) {
        this.$refs.uploadBtn.$el.click();
      }
    });
  },
  computed: {
    // Uploader实例
    uploader() {
      return this.$refs.uploader.uploader;
    },
  },
  methods: {
    // 自定义options
    customizeOptions(opts) {
      // 自定义上传url
      if (opts.target) {
        this.uploader.opts.target = opts.target;
        console.log("target:" + this.uploader.opts.target);
      }
      // 是否可以秒传、断点续传
      if (opts.testChunks !== undefined) {
        this.uploader.opts.testChunks = opts.testChunks;
      }
      // merge 的方法，类型为Function，返回Promise
      this.mergeFn = opts.mergeFn || mergeSimpleUpload;
      // 自定义文件上传类型
      let input = document.querySelector("#global-uploader-btn input");
      let accept = opts.accept || ACCEPT_CONFIG.getAll();
      input.setAttribute("accept", accept.join());
    },
    onFileAdded(file) {
      this.panelShow = true;
      this.emit("fileAdded");
      file.params = this.customParams; // 将额外的参数赋值到每个文件上，以不同文件使用不同params的需求
      this.computeMD5(file).then((result) => this.startUpload(result)); // 计算MD5
    },
    /**
     * 计算md5值，以实现断点续传及秒传
     * @param file
     * @returns Promise
     */
    // 根据文件内容计算 MD5 并标记文件 file
    computeMD5(file) {
      let fileReader = new FileReader();
      let time = new Date().getTime();
      let blobSlice =
        File.prototype.slice ||
        File.prototype.mozSlice ||
        File.prototype.webkitSlice;
      let currentChunk = 0;
      const chunkSize = 10 * 1024 * 1000;
      let chunks = Math.ceil(file.size / chunkSize);
      let spark = new SparkMD5.ArrayBuffer();

      // 文件状态设为"计算MD5"
      this.statusSet(file.id, "md5");
      file.pause();

      // 计算MD5时隐藏”开始“按钮
      this.$nextTick(() => {
        try {
          var obs = document.querySelector(
            `.file-${file.id} .uploader-file-resume`
          );
          obs.style.display = "none";
        } catch (e) {
          console.log(e);
        }
      });

      loadNext();

      return new Promise((resolve, reject) => {
        fileReader.onload = (e) => {
          spark.append(e.target.result);

          if (currentChunk < chunks) {
            currentChunk++;
            loadNext();
            // 实时展示MD5的计算进度
            this.$nextTick(() => {
              const md5ProgressText =
                "校验MD5 " + ((currentChunk / chunks) * 100).toFixed(0) + "%";
              var obj = document.querySelector(`.custom-status-${file.id}`);
              if (obj != null) {
                obj.innerText = md5ProgressText;
              }
            });
          } else {
            let md5 = spark.end();
            // md5计算完毕
            resolve({ md5, file });
            console.log(
              `MD5计算完毕：${file.name} \nMD5：${md5} \n分片：${chunks} 大小:${
                file.size
              } 用时：${new Date().getTime() - time} ms`
            );
          }
        };

        fileReader.onerror = function () {
          console.log("fileReader:" + `文件${file.name}读取出错，请检查该文件`);
          this.error(`文件${file.name}读取出错，请检查该文件`);
          file.cancel();
          reject();
        };
      });

      function loadNext() {
        let start = currentChunk * chunkSize;
        let end =
          start + chunkSize >= file.size ? file.size : start + chunkSize;

        fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
      }
    },
    // md5计算完毕，开始上传
    startUpload({ md5, file }) {
      file.uniqueIdentifier = md5;
      file.resume();
      this.statusRemove(file.id);
    },
    // 所有片段上传成功后，进入文件上传成功回调
    onFileSuccess(rootFile, file, response, chunk) {
      let res = JSON.parse(response);
      // 服务端自定义的错误（即http状态码为200，但是是错误的情况），这种错误是Uploader无法拦截的
      console.log(res);
      if (res == null) {
        this.error(res.msg + "111");
        // 文件状态设为“失败”
        this.statusSet(file.id, "failed");
        return;
      }
      // 如果服务端返回了需要合并的参数
      if (res.data.needMerge == 1) {
        // 文件状态设为“合并中”
        this.statusSet(file.id, "merging");
        // console.log(...file.params);
        // this.mergeFn({
        //   tempName: res.tempName,
        //   fileName: file.name,
        //   ...file.params,
        // })
        //   .then((res) => {
        //     // 文件合并成功
        //     this.emit("fileSuccess");
        //     this.statusRemove(file.id);
        //   })
        //   .catch((e) => {});
        this.service
          .post("/dict/ClothingPices/HeBing", res.data)
          .then((res) => {
            if (res.status == 200) {
              this.emit("fileSuccess");
              this.statusRemove(file.id);
            }
          })
          .catch((err) => {
            console.log(err);
          });
      } else {
        // 不需要合并
        this.emit("fileSuccess");
        console.log("上传成功");
      }
    },
    onFileProgress(rootFile, file, chunk) {
      console.log(
        `上传中 ${file.name}，chunk：${chunk.startByte / 1024 / 1024} ~ ${
          chunk.endByte / 1024 / 1024
        }`
      );
    },
    onFileError(rootFile, file, response, chunk) {
      console.log("onFileError：" + response);
      this.error(response);
    },
    close() {
      this.uploader.cancel();
      this.panelShow = false;
    },
    /**
     * 新增的自定义的状态: 'md5'、'merging'、'transcoding'、'failed'
     * @param id
     * @param status
     */
    statusSet(id, status) {
      let statusMap = {
        md5: {
          text: "校验MD5",
          bgc: "#fff",
        },
        merging: {
          text: "合并中",
          bgc: "#e2eeff",
        },
        transcoding: {
          text: "转码中",
          bgc: "#e2eeff",
        },
        failed: {
          text: "上传失败",
          bgc: "#e2eeff",
        },
      };

      this.$nextTick(() => {
        const statusTag = document.createElement("p");
        console.log(statusTag);
        statusTag.className = `custom-status-${id} custom-status`;
        statusTag.innerText = statusMap[status].text;
        statusTag.style.backgroundColor = statusMap[status].bgc;
        const statusWrap = document.querySelector(
          `.file_${id} .uploader-file-status`
        );
        if (statusWrap != null) {
          statusWrap.appendChild(statusTag);
        }
      });
    },
    statusRemove(id) {
      this.$nextTick(() => {
        try {
          const statusTag = document.querySelector(`.custom-status-${id}`);
          statusTag.remove();
        } catch (e) {
          console.log(e);
        }
      });
    },
    emit(e) {
      Bus.$emit(e);
      this.$emit(e);
    },
    error(msg) {
      console.log("msg:" + this.msg);
      this.$notify({
        title: "错误",
        message: msg,
        type: "error",
        duration: 3000,
      });
    },
  },
};
</script>

<style lang="scss">
#global-uploader {
  &:not(.global-uploader-single) {
    position: fixed;
    z-index: 20;
    right: 15px;
    bottom: 15px;
    box-sizing: border-box;
  }

  .uploader-app {
    width: 520px;
  }

  .file-panel {
    background-color: #fff;
    border: 1px solid #e2e2e2;
    border-radius: 7px 7px 0 0;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);

    .file-title {
      display: flex;
      height: 40px;
      line-height: 40px;
      padding: 0 15px;
      border-bottom: 1px solid #ddd;

      .operate {
        flex: 1;
        text-align: right;

        i {
          font-size: 18px;
        }
      }
    }

    .file-list {
      position: relative;
      height: 240px;
      overflow-x: hidden;
      overflow-y: auto;
      background-color: #fff;
      transition: all 0.3s;

      .file-item {
        background-color: #fff;
      }
    }

    &.collapse {
      .file-title {
        background-color: #e7ecf2;
      }
      .file-list {
        height: 0;
      }
    }
  }

  .no-file {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 16px;
  }

  .uploader-file-icon {
    &:before {
      content: "" !important;
    }

    &[icon="image"] {
      background: url(../assets/images/audio-icon.png);
    }
    &[icon="audio"] {
      background: url(../assets/images/audio-icon.png);
      background-size: contain;
    }
    &[icon="video"] {
      background: url(../assets/images/video-icon.png);
    }
    &[icon="document"] {
      background: url(../assets/images/text-icon.png);
    }
    &[icon="unknown"] {
      background: url(../assets/images/zip.png) no-repeat center;
      background-size: contain;
    }
  }

  .uploader-file-actions > span {
    margin-right: 6px;
  }

  .custom-status {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1;
  }
}

/* 隐藏上传按钮 */
#global-uploader-btn {
  position: absolute;
  clip: rect(0, 0, 0, 0);
}

.global-uploader-single {
  #global-uploader-btn {
    position: relative;
  }
}
</style>
